

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.bcel.Constants;
import org.apache.bcel.Repository;

import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.FieldInstruction;
import org.apache.bcel.generic.GETFIELD;
import org.apache.bcel.generic.InstructionConstants;
import org.apache.bcel.generic.InstructionFactory;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.InvokeInstruction;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.PUSH;
import org.apache.bcel.generic.ReturnInstruction;

import org.apache.bcel.generic.Type;
import org.apache.bcel.util.InstructionFinder;

public class Transform {
	private static final Type P_STREAM = new ObjectType("java.io.PrintStream");

	public static void main(String[] argv) {
		try {
			System.out.println("Transforming...");
			JavaClass clazz = Repository.lookupClass(argv[0]);
			Method[] methods = clazz.getMethods();
			ConstantPoolGen cp = new ConstantPoolGen(clazz.getConstantPool());

			for(int i=0; i < methods.length; i++) {
				if(!(methods[i].isAbstract() || methods[i].isNative())) {
					MethodGen mg = new MethodGen(methods[i], clazz.getClassName(), cp);
					Method wrapped = transform(mg);
					if(wrapped != null)
						methods[i] = wrapped;
				}
			}

			clazz.setConstantPool(cp.getFinalConstantPool());
			String className = clazz.getClassName();
			String shortName = className.substring(className.lastIndexOf('.') + 1);
			String fileName = shortName + ".class";
			
			clazz.dump(fileName);
			System.out.println("done.");
		} catch(Exception e) { e.printStackTrace(); }
	}
	
	private static String getBeforeString(InvokeInstruction invoke, ConstantPoolGen cpg) {
		return "About to call: " + invoke.getMethodName(cpg) + " " + invoke.getSignature(cpg);
	}

	private static InstructionList logTop(MethodGen mg, Type topType) {
		InstructionList postIl = new InstructionList();
		InstructionFactory factory = new InstructionFactory(mg.getConstantPool());
		Type sigType = getPrintSigType(topType);

		postIl.append(InstructionFactory.createDup(topType.getSize()));
		postIl.append(tuckUnder(topType, factory.createFieldAccess("java.lang.System", "out", P_STREAM, Constants.GETSTATIC)));
		
		
		if (!topType.equals(Type.STRING)) {
			postIl.append(factory.createInvoke("java.lang.String", "valueOf",
					Type.STRING, new Type[] {sigType}, Constants.INVOKESTATIC));
		}
		
		postIl.append(factory.createInvoke("java.io.PrintStream", "println",
				Type.VOID, new Type[] { Type.STRING },
                Constants.INVOKEVIRTUAL));
		return postIl;
	}

	private static final Method transform(MethodGen mg) {		
		InstructionList il = mg.getInstructionList();
		InstructionFactory factory = new InstructionFactory(mg.getConstantPool());
		ConstantPoolGen cpg = mg.getConstantPool();
		
		//printInfo(mg);
		//System.out.println(mg.getName());
		for (InstructionHandle getFieldInstr : getGetFields(il)) {
			GETFIELD gf = ((GETFIELD)getFieldInstr.getInstruction());
			il.insert(getFieldInstr, InstructionFactory.createDup(1));
			il.insert(getFieldInstr, printingByteCode(mg, "Before getfield:\n    ", false));
			il.insert(getFieldInstr, printingByteCode(mg, gf.getClassType(cpg).toString()));
			il.insert(getFieldInstr, printingByteCode(mg, "    ", false));
			il.insert(getFieldInstr, printingByteCode(mg, gf.getFieldType(cpg).toString()));
			il.insert(getFieldInstr, printingByteCode(mg, "    ", false));
			il.insert(getFieldInstr, printingByteCode(mg, gf.getFieldName(cpg).toString()));
			il.insert(getFieldInstr, printingByteCode(mg, "    ", false));
			
			Type fieldType = gf.getFieldType(cpg);
			InstructionList after = new InstructionList();
			after.append(InstructionFactory.createDup(fieldType.getSize()));
			
			after.append(printTop(mg, fieldType));
			il.append(getFieldInstr, after);
		}
		//printInfo(mg);

		mg.setMaxStack();
		Method m = mg.getMethod();
		il.dispose(); // Reuse instruction handles
		return m;
	}
	
	private static InstructionList printTop(MethodGen mg, Type type) {
		InstructionList il = logTop(mg, type);
		il.append(InstructionFactory.createPop(type.getSize()));
		return il;
	}


	private static InstructionList printingByteCode(MethodGen mg, String output) {
		return printingByteCode(mg, output, true);
	}

	private static InstructionList printingByteCode(MethodGen mg, String output, Boolean newLine) {
		InstructionFactory factory = new InstructionFactory(mg.getConstantPool());		
		InstructionList il = new InstructionList();
		il.append(factory.createFieldAccess("java.lang.System", "out", P_STREAM, Constants.GETSTATIC));
		il.append(new PUSH(mg.getConstantPool(), output));
		il.append(factory.createInvoke("java.io.PrintStream", newLine ? "println" : "print",
				Type.VOID, new Type[] { Type.STRING },
	            Constants.INVOKEVIRTUAL));
		return il;
	}
		

	private static void postWrap(MethodGen mg) {
		if (mg.getReturnType().equals(Type.VOID))
			return;		
		InstructionList il = mg.getInstructionList();
		List<InstructionHandle> returns = getReturns(il);
		for (InstructionHandle instrHandle : returns) {
			il.insert(instrHandle, logTop(mg, mg.getReturnType()));
		}
	}

	private static InstructionList tuckUnder(Type topType, FieldInstruction instr) {
		InstructionList ret = new InstructionList();
		ret.append(instr);
		if (topType.getSize() == 2) {
			ret.append(InstructionConstants.DUP_X2);
			ret.append(InstructionConstants.POP);
		} else {
			ret.append(InstructionConstants.SWAP);
		}
		return ret;		
	}

	private static Type getPrintSigType(Type type) {
		if (!isPrimitive(type))
			return Type.OBJECT;
		if (isIntegerish(type))
			return Type.INT;
		return type;
	}

	private static boolean isIntegerish(Type type) {
		return type.equals(Type.BYTE) || type.equals(Type.SHORT);
	}

	private static boolean isPrimitive(Type type) {
		return  type.equals(Type.BYTE) || type.equals(Type.SHORT) ||
				type.equals(Type.INT) || type.equals(Type.LONG) ||
				type.equals(Type.FLOAT) || type.equals(Type.DOUBLE) ||
				type.equals(Type.BOOLEAN) || type.equals(Type.CHAR);
	}

	private static List<InstructionHandle> getReturns(InstructionList il) {
		List<InstructionHandle> returns = new ArrayList<InstructionHandle>();
		for (Iterator<InstructionHandle> iter = il.iterator(); iter.hasNext(); ) {
			InstructionHandle nextInstr = iter.next();
			if (nextInstr.getInstruction() instanceof ReturnInstruction) {
				returns.add(nextInstr);
			}
		}
		return returns;
	}

	private static List<InstructionHandle> getGetFields(InstructionList il) {
		List<InstructionHandle> returns = new ArrayList<InstructionHandle>();
		for (Iterator<InstructionHandle> iter = il.iterator(); iter.hasNext(); ) {
			InstructionHandle nextInstr = iter.next();
			if (nextInstr.getInstruction() instanceof GETFIELD) {
				returns.add(nextInstr);
				//System.out.print("#  ");
			}
			//System.out.println(nextInstr);
		}
		return returns;
	}


	public static void printInfo(MethodGen mg) {
		System.out.println(mg.getName());
		
		InstructionList il = mg.getInstructionList();
		for (Iterator<InstructionHandle> iter = il.iterator(); iter.hasNext();) {
			InstructionHandle i = iter.next();
			System.out.println(i);
		}
	}
}